<?php
/**
 * @author		João Batista Neto
 * @brief		Classes e interfaces utilitárias
 * @package		rpo.util
 */

require_once 'rpo/base/Object.php';
require_once 'rpo/util/AbstractMapReduce.php';

/**
 * @brief	Interface para um Iterator de objetos
 * @class	ObjectIterator
 */
abstract class ObjectIterator extends AbstractMapReduce implements ArrayAccess, Iterator, Countable, OuterIterator, SeekableIterator, Serializable {
	/**
	 * @brief	Lista de objetos
	 * @var		ArrayIterator
	 */
	protected $storage;

	/**
	 * @brief	Constroi o objeto Iterator
	 */
	public function __construct( ObjectIterator $iterator = null ){
		if ( !is_null( $iterator ) ){
			$this->storage = $iterator;
		} else {
			$this->storage = new ArrayIterator();
		}
	}

	/**
	 * @brief	Verifica se um objeto é aceitável para o Iterator antes de adicioná-lo
	 * @return	boolean
	 */
	abstract protected function accept( IObject $object );

	/**
	 * @brief	Adiciona um novo objeto ao Iterator
	 * @param	$object IObject
	 * @throws	InvalidArgumentException Se o Iterator não aceitar o objeto
	 */
	public function append( IObject $object ){
		if ( $this->accept( $object ) ){
			$this->storage->append( $object );
		} else {
			throw new InvalidArgumentException( sprintf( 'O Iterator não pode aceitar objetos %s' , $object->getClass()->getName() ) );
		}
	}

	/**
	 * @brief	Recupera o total de itens do Iterator
	 * @return	integer
	 * @see		Countable::count()
	 */
	public function count(){
		return $this->storage->count();
	}

	/**
	 * @brief	Recupera o item atual do iterator
	 * @return	IObject
	 * @see		Iterator::current()
	 */
	public function current(){
		return $this->storage->current();
	}

	/**
	 * @brief	Executa o passo de mapeamento
	 * @param	$step MapStep Objeto que fará o mapeamento
	 */
	protected function doMap( MapStep $step  ){
		for ( $this->rewind() ; $this->valid() ; $this->next() ){
			$step->map( $this , $this->key() , $this->current() );
		}
	}

	/**
	 * @brief	Executa o passo de mapeamento
	 * @param	$step ReduceStep Objeto que fará o mapeamento
	 */
	protected function doReduce( ReduceStep $step ){
		$iterator = $this->mapped;

		for ( $iterator->rewind() ; $iterator->valid() ; $iterator->next() ){
			$step->reduce( $this , $iterator->key() , $iterator->current() );
		}
	}

	/**
	 * @brief	Recupera o Iterator do objeto
	 * @return	Iterator
	 * @see		OuterIterator::getInnerIterator()
	 */
	public function getInnerIterator(){
		return $this->storage;
	}

	/**
	 * @brief	Recupera a chave ou índice do item atual
	 * @return	mixed
	 * @see		Iterator::key()
	 */
	public function key(){
		return $this->storage->key();
	}

	/**
	 * @brief	Avança o Iterator ao próximo item
	 * @see		Iterator::next()
	 */
	public function next(){
		$this->storage->next();
	}

	/**
	 * @brief	Verifica se um índice existe no Iterator
	 * @return	boolean
	 * @see		ArrayAccess::offsetExists()
	 */
	public function offsetExists( $offset ){
		return $this->storage->offsetExists( $offset );
	}

	/**
	 * @brief	Recupera um item do Iterator
	 * @return	IObject
	 * @see		ArrayAccess::offsetGet()
	 * @throws	OutOfBoundsException Se o offset especificado não existir
	 */
	public function offsetGet( $offset ){
		if ( !$this->storage->offsetExists( $offset ) ){
			throw new OutOfBoundsException( 'O offset não existe' );
		} else {
			return $this->storage->offsetGet( $offset );
		}
	}

	/**
	 * @brief	Define um novo item no Iterator utilizando um offset absoluto
	 * @see		ArrayAccess::offsetSet()
	 * @throws	UnexpectedValueException Se o novo valor não implementar IObject
	 */
	public function offsetSet( $offset , $object ){
		if ( $object instanceof IObject ){
			$this->storage->offsetSet( $offset , $object );
		} else {
			throw new UnexpectedValueException( sprintf( 'Era esperado um objeto, %s foi dado.' , gettype( $object ) ) );
		}
	}

	/**
	 * @brief	Remove um item do Iterator
	 * @see		ArrayAccess::offsetUnset()
	 * @throws	OutOfBoundsException Se o offset especificado não existir
	 */
	public function offsetUnset( $offset ){
		if ( !$this->storage->offsetExists( $offset ) ){
			throw new OutOfBoundsException( 'O offset não existe' );
		} else {
			$this->storage->offsetUnset( $offset );
		}
	}

	/**
	 * @brief	Reinicia o Iterator
	 * @see		Iterator::rewind()
	 */
	public function rewind(){
		$this->storage->rewind();
	}

	/**
	 * @brief	Avança o Iterator à uma posição específica
	 * @see		SeekableIterator::seek()
	 * @throws	OutOfBoundsException Se o offset especificado não existir
	 */
	public function seek( $offset ){
		if ( !$this->storage->offsetExists( $offset ) ){
			throw new OutOfBoundsException( 'O offset não existe' );
		} else {
			$this->storage->seek( $offset );
		}
	}

	/**
	 * @brief	Serializa o objeto
	 * @return	string
	 * @see		Serializable::serialize()
	 */
	public function serialize(){
		return $this->storage->serialize();
	}

	/**
	 * @brief	Retorna a string serializada ao objeto original
	 * @see		Serializable::unserialize()
	 */
	public function unserialize( $serialized ){
		$this->storage->unserialize( $serialized );
	}

	/**
	 * @brief	Verifica se o Iterator ainda é válido
	 * @see		Iterator::valid()
	 */
	public function valid(){
		return $this->storage->valid();
	}
}